package sketch.ounit.fuzz;

import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.jdt.core.dom.Modifier;

import randoop.ComponentManager;
import randoop.ExecutableSequence;
import randoop.ExecutionOutcome;
import randoop.NormalExecution;
import randoop.RMethod;
import randoop.Sequence;
import randoop.StatementKind;
import randoop.Variable;
import randoop.main.GenInputsAbstract;
import randoop.util.MethodReflectionCode;
import randoop.util.Reflection;
import randoop.util.ReflectionCode;
import randoop.util.ReflectionExecutor;
import randoop.util.SimpleList;
import sketch.util.Checker;
import sketch.util.Globals;
import sketch.util.Randomness;
import sketch.util.RandoopUtil;
import sketch.util.SketchLog;

public class AssembledSequenceWithArgs<T> extends AssembledSequence<T> {

	private final Object[] args;
	
	//all the method argument
	protected List<List<MethodArg>> method_args = new LinkedList<List<MethodArg>>();
	
	
	public AssembledSequenceWithArgs(List<RMethod> rmethods, T receiver,
			Class<?> clazz, ComponentManager components, Object[] args) {
		super(rmethods, receiver, clazz, components);
		Checker.checkNull(args, "The args parameter could not be null.");
		this.args = args;
	}
	
	@Override
	protected int size() {
		int length = 0;
		for(List<MethodArg> arg_list : method_args) {
			for(MethodArg arg : arg_list) {
				if(arg.isExecutableSequence()) {
					length += arg.getESeq().sequence.size();
				}
			}
		}
		return length;
	}
	
	@Override
	protected T assemble_and_execute_sequence() {
		int method_extended = 0;
		int sequence_length = 0;
		T receiver_obj = super.recevier;
		//pick a number of methods
		while(method_extended < super.extend_times && sequence_length < GenInputsAbstract.maxsize) {
			RMethod rmethod = Randomness.randomMember(this.all_methods);
			Method method = rmethod.getMethod();
			//find sequences for each method
			List<MethodArg> params = new LinkedList<MethodArg>();
			Class<?>[] required_types = method.getParameterTypes();
			//the object array
			Object[] param_objs = new Object[required_types.length];
			//choose sequences for each required type
			int param_index = 0;
			for(Class<?> required_type : required_types) {
				//first look at the given object to see if there is any compatible args
				List<Integer> i_list = new LinkedList<Integer>();
				for(int i = 0; i < args.length; i++) {
					if(args[i] == null) {
						continue;
					} else {
						Class<?> arg_type = args[i].getClass();
						if(!Modifier.isPublic(arg_type.getModifiers())) {
							continue;
						} else {
							if(Reflection.canBeUsedAs(arg_type, required_type)) {
								i_list.add(i);
							}
						}
					}
				}
				//get a list of compatible sequences
				SimpleList<Sequence> list = this.components.getSequencesForType(required_type, false);
				if(list.isEmpty() && i_list.isEmpty()) {
					SketchLog.logln("Both arg object and sequence list is empty, no sequence found for type: " + required_type);
					//return the current receiver_obj
					return receiver_obj;
				}
				//decide to choose passing args or the executable sequences
				int choose_index = -1;
				Sequence seq = null;
				if(!i_list.isEmpty()) {
					choose_index = Randomness.randomMember(i_list);
				}
				if(!list.isEmpty()) {
					seq = Randomness.randomMember(list.toJDKList());
				}
				boolean use_index = false;
				if(choose_index != -1) {
					use_index = Randomness.nextRandomBool();
				    if(!use_index && seq == null) {
				    	use_index = true;
				    }
				}
				if(!use_index) {
					Checker.checkNull(seq, "The sequence could not be null, when do not use index.");
				}
				//create the parameter
				if(use_index) {
					Object obj = this.args[choose_index];
					param_objs[param_index] = obj;
					//add the parameter
					params.add(new MethodArg(choose_index));
				} else {
					ExecutableSequence eSeq = new ExecutableSequence(seq);
					eSeq.execute(null);
					//check which index could be used
					List<Variable> lastVars = seq.getLastStatementVariables();
					List<Variable> all_compatibles = new LinkedList<Variable>();
					for(Variable var : lastVars) {
						Class<?> var_type = seq.getStatementKind(var.getDeclIndex()).getOutputType();
						if(Reflection.canBeUsedAs(var_type, required_type)) {
							all_compatibles.add(var);
						}
					}
					Checker.checkTrue(!all_compatibles.isEmpty(), "The compatibles should not be empty.");
					Variable var = Randomness.randomMember(all_compatibles);
					//set the param
					ExecutionOutcome outcome = eSeq.getResult(var.getDeclIndex());
					Checker.checkTrue(outcome instanceof NormalExecution, "The outcome should be a normal execution");
					param_objs[param_index] = ((NormalExecution)outcome).getRuntimeValue();
					//add the parameter
					params.add(new MethodArg(eSeq, var.getDeclIndex()));
				}
				//increase the index
				param_index++;
			}
			int repeat_times = 1;
			if(Randomness.nextRandomInt(10) == 0) {
				repeat_times = Randomness.nextRandomInt(max_repetition_times) + 1;
			}
			
			//execute it for n times
			Throwable thrown_exception = null;
			for(int time = 1; time <=repeat_times; time++) {
				ReflectionCode code = new MethodReflectionCode(method, receiver_obj, param_objs);
				Throwable e = ReflectionExecutor.executeReflectionCode(code, System.out);
				if(e != null) {
					thrown_exception = e;
					repeat_times = time;
					//XXX break or not?
					break;
				}
			}
			//add to the sequence
			this.extended_methods.add(rmethod);
			this.method_args.add(params);
			this.repetitions.add(repeat_times);
			//if there is no exception we continue, or else we break;
			if(thrown_exception != null) {
				Checker.checkTrue(this.thrown_exp == null, "The current thrown exception should be null.");
				this.thrown_exp = thrown_exception;
				break;
			}
			//the method number update
			method_extended++;
			for(MethodArg arg : params) {
			  sequence_length += arg.size();
			}
		}//continue to select another method
		
		return receiver_obj;
	}
	
	@Override
	public String toCodeString(int offset, String receiver_name) {
		throw new RuntimeException("Should not call toCodeString(int,String) in AssembledSequenceWithArgs class.");
	}
	
	@Override
	public String toString() {
		String receiver_name = "%receiver_name%";
		String[] args_names = new String[this.args.length];
		String[] expr_names = new String[this.args.length];
		for(int i = 0; i < args_names.length; i++) {
			args_names[i] = "%arg_name_" +  i + "%";
			expr_names[i] = "%expr_name_" + i + "%";
		}
		return toCodeString(0, receiver_name, args_names, expr_names);
	}
	
	@Override
	public String toCodeString() {
		super.checkToCodeStringSetUp();
		return this.toCodeString(this.offset, this.receiver_name, this.arg_names, this.expression_names);
	}
	
	protected String toCodeString(int offset, String receiver_name, String[] argNames, String[] exprNames) {
		Checker.checkNull(receiver_name, "The receiver name could not be null.");
		Checker.checkNull(argNames, "Arg names could not be null.");
		for(String argName : argNames) {
			Checker.checkNull(argName, "The arg name could not be null");
		}
		Checker.checkTrue(argNames.length == exprNames.length && argNames.length == this.args.length,
				"The length of arg names: " + argNames.length + " is different from exprnames: " + exprNames.length
				+ ", and the args: " + this.args.length);
		this.checkAssembleSequenceValidity();
		//concatenate all
		List<Sequence> all_sequences = new LinkedList<Sequence>();
		for(List<MethodArg> arg_list : this.method_args) {
			for(MethodArg arg : arg_list) {
				if(arg.isExecutableSequence()) {
					all_sequences.add(arg.getESeq().sequence);
				}
			}
		}
		Sequence concatenate = Sequence.concatenate(all_sequences);
		//append selected methods inside the concatenated sequence
		StringBuilder sb = new StringBuilder();
		//append the var declaration here
		sb.append("// the var declaration");
		sb.append(Globals.lineSep);
		for(int i = 0 ; i < this.args.length; i++) {
			Class<?> c = this.args[i].getClass();
			sb.append(c.getName().toString() + " " + argNames[i] + " = " + exprNames[i] + ";");
			sb.append(Globals.lineSep);
		}
		int stmt_index = 0;
		int sequence_length = 0;
		for(int i = 0; i < this.method_args.size(); i++) {
			boolean isTheLastMethod = (i == this.method_args.size() - 1);
			//get the invoke method
			RMethod rmethod = this.extended_methods.get(i);
			Method method = rmethod.getMethod();
			List<MethodArg> method_arg_list = this.method_args.get(i);
			int repetition = this.repetitions.get(i);
			Checker.checkTrue(repetition > 0, "repetition: " + repetition);
			//first append, all the parameters
			int length = 0;
			for(MethodArg arg : method_arg_list) {
				if(arg.isExecutableSequence()) {
				  length = length + arg.size();
				}
			}
			for(int append_index = stmt_index; append_index < stmt_index + length; append_index++) {
//				concatenate.printStatement(sb, append_index);
				//rename it
				RandoopUtil.appendSequenceStatementWithOffset(offset, concatenate, sb, append_index);
			}
			//append the invoked method
			List<String> arg_names = new LinkedList<String>();
			int current_index = 0;
			for(MethodArg arg : method_arg_list) {
				if(arg.isPassedArg()) {
					arg_names.add(argNames[arg.getIndex()]);
				} else {
					int relative_idx = arg.getESeqIndex();
					current_index = sequence_length + relative_idx;
					Variable var = concatenate.getVariable(current_index);
					sequence_length += arg.getESeq().sequence.size();
					var = new Variable(var.sequence, var.index + offset);
					arg_names.add(var.toString());
				}
			}
			while(repetition -- != 0) {
			  //check to see if any exception wrapping is needed
			  boolean needToWrap = ((repetition == 0) && this.hasException() && isTheLastMethod);
			  if(needToWrap) {
				  Checker.checkNull(this.getException(), "The thrown exception should not be null.");
				  sb.append("try {");
				  sb.append(Globals.lineSep);
			  } //else if (repetition == 0) {
				//  Checker.checkTrue(this.getException() == null, "The thrown exception should be null.");
			  //}
			  //print out the method
			  if(rmethod.isStatic()) {
				sb.append(this.receiver_class.getName());
			  } else {
				  sb.append(receiver_name);
			  }
			  sb.append(".");
			  sb.append(method.getName());
			  sb.append("(");
			  for(int argIndex = 0; argIndex < arg_names.size(); argIndex++) {
			    if(argIndex!=0) {
				  sb.append(", ");
				}
			    String argname = arg_names.get(argIndex);
			    //do the renmaing
				sb.append(argname);
			  }
			  sb.append(");");
			  sb.append(Globals.lineSep);
			  if(needToWrap) {
				sb.append("  fail(\" exception: " + this.getException().toString() + " should be thrown \");" );
				sb.append(Globals.lineSep);
			    sb.append("} catch (" + this.getException().getClass().getName() + " e) {");
			    sb.append(Globals.lineSep);
			    sb.append("  //ok here");
			    sb.append(Globals.lineSep);
			    sb.append("}");
			    sb.append(Globals.lineSep);
			  }
			}
			
			//adjust the statement index
			stmt_index = stmt_index + length;
		}
		
		return sb.toString();
	}
	
	@Override
	protected void checkAssembleSequenceValidity() {
		Checker.checkTrue(super.method_params.isEmpty(), "Method param is empty");
		Checker.checkTrue(this.extended_methods.size() == this.method_args.size(), "The extended method size: "
				+ extended_methods.size() + " != the size of method args: " + this.method_args.size());
		for(int i = 0; i < this.extended_methods.size(); i++) {
			RMethod rmethod = this.extended_methods.get(i);
			List<MethodArg> params = this.method_args.get(i);
			int repetition = this.repetitions.get(i);
			Checker.checkTrue(repetition >= 1, "Invalid repetition value: " + repetition);
			//check each pair
			Checker.checkTrue(params.size() == rmethod.getMethod().getParameterTypes().length,
					"The param size: " + params.size() + " should == the param type length: "
					+ rmethod.getMethod().getParameterTypes().length);
			Class<?>[] required_types = rmethod.getMethod().getParameterTypes();
			for(int arg_index = 0; arg_index < params.size(); arg_index++) {
				MethodArg arg = params.get(arg_index);
				if(arg.isPassedArg()) {
					Checker.checkTrue(Reflection.canBeUsedAs(this.args[arg.getIndex()].getClass(), required_types[arg_index]), "Error");
				} else {
					StatementKind statement = arg.getESeq().sequence.getStatementKind(arg.getESeqIndex());
					Class<?> output_type = statement.getOutputType();
					//check the type compatibility
					Checker.checkTrue(Reflection.canBeUsedAs(output_type, required_types[arg_index]), "The output type: " + output_type
							+ " could not be used as requried_type: " + required_types[arg_index]);
				}
			}
		}
	}
	
	@Override
	public int hashCode() {
		int hashcode = 0;
		hashcode += 3*receiver_class.hashCode();
		for(RMethod rmethod : extended_methods) {
			hashcode += 5*rmethod.hashCode();
		}
		Checker.checkTrue(method_params.isEmpty(), "The method para should be empty.");
		for(List<MethodArg> argList : method_args) {
			for(MethodArg arg : argList) {
				hashcode += 13*arg.hashCode();
			}
		}
		Checker.checkTrue(param_indices.isEmpty(), "The method param index should be emtpy.");
		
		for(Integer repetition : repetitions) {
			hashcode += 23*repetition;
		}
		return hashcode;
	}
	
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof AssembledSequenceWithArgs)) {
			return false;
		}
		AssembledSequenceWithArgs<?> other = (AssembledSequenceWithArgs<?>)obj;
		return this.receiver_class.equals(other.receiver_class)
		     && this.extended_methods.equals(other.extended_methods)
		     && this.method_args.equals(other.method_args)
		     && this.repetitions.equals(other.repetitions);
	}
	
	/**
	 * A class wrapping possible method argument. Given the passed object, an argument
	 * could either be the index or a new picked executable sequence
	 * */
	public class MethodArg {
		private int index = -1;
		
		private ExecutableSequence eseq = null;
		private int eseq_index = -1;
		
		public MethodArg(int index) {
			Checker.checkTrue(index >= 0 && index < args.length, "The index is not valid: " + index
					+ ", the object size is: " + args.length);
			this.index = index;
		}
		public MethodArg(ExecutableSequence eseq, int eseq_index) {
			Checker.checkNull(eseq, "The executable sequence could not be null.");
			this.eseq = eseq;
			this.eseq_index = eseq_index;
		}
		public boolean isPassedArg() {
			return index != -1;
		}
		public boolean isExecutableSequence() {
			return eseq != null;
		}
		public int getIndex() {
			return index;
		}
		public ExecutableSequence getESeq() {
			if(eseq != null) {
				Checker.checkTrue(eseq_index != -1, "The eseq index: " + eseq_index + " is not valid.");
			}
			return eseq;
		}
		public int getESeqIndex() {
			if(this.eseq_index != -1) {
				Checker.checkNull(eseq, "The executable sequence should not be null.");
			}
			return this.eseq_index;
		}
		public int size() {
			if(this.isPassedArg()) {
				return 1;
			} else {
				return this.eseq.sequence.size();
			}
		}
		@Override
		public int hashCode() {
			int code = 0;
			code += index*13;
			code += eseq_index*53;
			if(eseq != null) {
				code += 23 * eseq.hashCode();
			} else {
				code += 47;
			}
			return code;
		}
		@Override
		public boolean equals(Object obj) {
			if(!(obj instanceof AssembledSequenceWithArgs.MethodArg)) {
				return false;
			}
			MethodArg arg = (MethodArg)obj;
			return arg.index == this.index && (eseq != null ? (eseq.equals(arg.eseq)&& eseq_index == arg.eseq_index) : arg.eseq == null);
		}
		@Override
		public String toString() {
			if(index != -1) {
				return " obj index: " + index;
			} else {
				return " executable sequence with seq index: " + eseq_index;
			}
		}
	}
}
